package com.magenta.maxoptra.gis.service;

import com.magenta.commons.datetime.DateTimeConstants;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Logger;
import com.magenta.maxoptra.gis.util.Apis;

import javax.ejb.EJB;
import javax.ejb.Local;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author Sergey Grachev
 */
@Local(CachesService.class)
@TransactionManagement(TransactionManagementType.BEAN)
public class CachesServiceImpl implements CachesService, CachesServiceMBean {

    private static final Logger LOG = new Logger(CachesService.class);

    private static final Comparator<CleanTask> SCHEDULE_COMPARATOR = new Comparator<CleanTask>() {
        @Override
        public int compare(final CleanTask o1, final CleanTask o2) {
            return o1.nextExecutionTime == o2.nextExecutionTime ? 0 :
                    o1.nextExecutionTime > o2.nextExecutionTime ? 1 : -1;
        }
    };
    private final AtomicBoolean serviceStopped = new AtomicBoolean(false);
    private final List<CleanTask> scheduled = Collections.synchronizedList(new ArrayList<CleanTask>());
    private final Object noWork = new Object();
    private Thread cleanThread;

    @EJB
    private SyncGeocodingService geocodingService;

    @EJB
    private SyncGeoRoutingService geoRoutingService;

    @Override
    public void start() {
        LOG.debug("Started %s", CachesService.class.getSimpleName());

        synchronized (serviceStopped) {
            cleanThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!serviceStopped.get() && cleanThread.isAlive() && !cleanThread.isInterrupted()) {

                        // get immutable list of tasks
                        final CleanTask[] list = scheduled.toArray(new CleanTask[scheduled.size()]);

                        for (final CleanTask cleanTask : list) {
                            try {
                                cleanTask.nextExecutionTime = Math.min(
                                        geocodingService.cleanAndGetDateOfFirstOldest(cleanTask.cid, cleanTask.maxAge),
                                        geoRoutingService.cleanAndGetDateOfFirstOldest(cleanTask.cid, cleanTask.maxAge)
                                ) + cleanTask.maxAge;
                            } catch (Exception e) {
                                LOG.trace("Cache clean", e);
                            }
                        }

                        // resort by nextExecutionTime
                        synchronized (scheduled) {
                            Collections.sort(scheduled, SCHEDULE_COMPARATOR);
                        }

                        try {
                            synchronized (noWork) {
                                final long timeout;
                                synchronized (scheduled) {
                                    timeout = scheduled.isEmpty() ? Long.MAX_VALUE
                                            : (scheduled.get(0).nextExecutionTime  - System.currentTimeMillis());
                                }
                                noWork.wait(timeout >= 0 ? timeout : Long.MAX_VALUE);
                            }
                        } catch (InterruptedException e) {
                            break;
                        }
                    }
                }
            });
            cleanThread.setDaemon(true);
            cleanThread.setPriority(Thread.MIN_PRIORITY);
            cleanThread.start();
        }
    }

    @Override
    public void stop() {
        synchronized (serviceStopped) {
            serviceStopped.set(true);
            if (cleanThread != null) {
                cleanThread.interrupt();
                cleanThread = null;
            }
        }
        LOG.debug("Stopped %s", CachesService.class.getSimpleName());
    }

    @Override
    public void scheduleCleanForCID(final long cid, final long maxAge) {
        final CleanTask cleanTask = new CleanTask(cid, maxAge);

        // add or replace exists
        synchronized (scheduled) {
            scheduled.remove(cleanTask);
            scheduled.add(cleanTask);
            Collections.sort(scheduled, SCHEDULE_COMPARATOR);
        }

        // notify thread
        synchronized (noWork) {
            noWork.notify();
        }
    }

    @Override
    public void scheduleCleanUsingSettings(final GeoProviderSettings settings) {
        if (settings.getProviderName() == null) {
            return;
        }

        final long cid = Apis.cid(settings.getProviderName(), settings.getVersionApi(), settings.getCidDiscriminator());
        final String cacheStorageTimeString = settings.getGisParam(GisParameter.cacheStorageTime.withProvider(settings.getProviderName().toLowerCase()));
        if (cacheStorageTimeString == null) {
            return;
        }

        try {
            final long cacheStorageTime = Integer.parseInt(cacheStorageTimeString) * DateTimeConstants.MSECS_IN_DAY;
            scheduleCleanForCID(cid, cacheStorageTime);
        } catch (NumberFormatException ignore) {
            LOG.trace("Incorrect format of gis parameter cacheStorageTime %s", cacheStorageTimeString);
        }
    }

    private static final class CleanTask {
        public final long maxAge;
        public final long cid;
        public long nextExecutionTime = 0;

        public CleanTask(final long cid, final long maxAge) {
            this.maxAge = maxAge;
            this.cid = cid;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof CleanTask)) {
                return false;
            }
            final CleanTask cleanTask = (CleanTask) o;
            return cid == cleanTask.cid;
        }

        @Override
        public int hashCode() {
            return (int) (cid ^ (cid >>> 32));
        }
    }
}
